﻿namespace Bowlus.Common
{
    using System;
    using System.Diagnostics;
    using System.Linq;
    using System.ServiceModel;
    using System.Threading;
    using Microsoft.WindowsAzure.ServiceRuntime;

    public enum NotifyMessageType
    {
        Deploy = 0,
        Rebalance = 1,
        UpdateUrl = 2,
        UpdateArr = 3
    }

    /*
     * This is the only service contract: Client notify Server a string msg. 
     * Fire and forget. Client doesn't get blocked.
     * Server take action based on the simple string msg. The actural server action is defined in the Handle delegate.
     */ 
    [ServiceContract(Namespace = "Bowlus.NotifyService")]
    public interface INotifyService
    {
        // Note: this method should NOT be blocked on the server side. See 'Server Side' logic.
        [OperationContract(IsOneWay = true)]
        void Notify(NotifyMessageType msg);
    }

    /*
     * Client side logic. 
     * Both Roles (FrontEnd/AppHost) can be the client. Both Roles can be the server.
     */
    public class NotifyModuleClient : IDisposable
    {
        // Bowlus.FrontEnd, Bowlus.AppHost
        // Use role name as endpoint name
        private readonly string clientRoleName;

        // Bowlus.FrontEnd, Bowlus.AppHost
        // Use role name as endpoint name
        private readonly string serverRoleName;

        private static readonly NetTcpBinding Binding = new NetTcpBinding(SecurityMode.None);

        private readonly ChannelFactory<INotifyService> factory = new ChannelFactory<INotifyService>(Binding);

        public NotifyModuleClient()
        {
            this.clientRoleName = RoleEnvironment.CurrentRoleInstance.Role.Name;
            this.serverRoleName = this.clientRoleName == Settings.FrontEndRoleName ? Settings.AppHostRoleName : Settings.FrontEndRoleName;
        }

        // notify all instances in another role
        public void NotifyAllInstancesInAnotherRole(NotifyMessageType message)
        {
            var messageTypeStr = Enum.GetName(typeof(NotifyMessageType), message);

            var endPoints = RoleEnvironment.Roles[this.serverRoleName].Instances.Select(instance => instance.InstanceEndpoints[this.serverRoleName]);

            foreach (var ep in endPoints)
            {
                var address = new EndpointAddress(
                    string.Format("net.tcp://{0}/{1}", ep.IPEndpoint, this.serverRoleName));
                var client = this.factory.CreateChannel(address);

                try
                {
                    if (client != null)
                    {
                        Trace.TraceInformation(string.Format("*** Notify {0}/{1}: {2}", ep.IPEndpoint, this.serverRoleName, messageTypeStr));

                        client.Notify(message);  
                        ((ICommunicationObject)client).Close();
                    }
                }
                catch (Exception ex)
                {
                    Trace.TraceError(ex.Message);
                    var communicationObject = (ICommunicationObject)client;
                    if (communicationObject != null)
                    {
                        communicationObject.Abort();
                    }
                }
            }
        }

        // notify all other instances in this same role
        public void NotifyAllOtherInstancesInThisRole(NotifyMessageType message)
        {
            var messageTypeStr = Enum.GetName(typeof(NotifyMessageType), message);

            var endPoints = RoleEnvironment.Roles[this.clientRoleName].Instances.Select(instance => instance.InstanceEndpoints[this.clientRoleName]);
            var myEndPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[this.clientRoleName];

            foreach (var ep in endPoints)
            {
                if (ep.IPEndpoint.ToString().Equals(myEndPoint.IPEndpoint.ToString())) continue;
                var address = new EndpointAddress(
                    string.Format("net.tcp://{0}/{1}", ep.IPEndpoint, this.clientRoleName));
                var client = this.factory.CreateChannel(address);

                try
                {
                    if (client != null)
                    {
                        Trace.TraceInformation(string.Format("*** Notify {0}/{1}: {2}", ep.IPEndpoint, this.clientRoleName, messageTypeStr));

                        client.Notify(message);
                        ((ICommunicationObject)client).Close();
                    }
                }
                catch (Exception ex)
                {
                    Trace.TraceError(ex.Message);
                    var communicationObject = (ICommunicationObject)client;
                    if (communicationObject != null)
                    {
                        communicationObject.Abort();
                    }
                }
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (null != factory)
                {
                    factory.Close();
                }
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    /*
     * Server side logic.
     */

    // Defines the actual action taken, when the server is notified.
    public delegate void NotifyHandle(NotifyMessageType msg);
    
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    public class NotifyModuleServer : INotifyService
    {
        private NotifyHandle TheNotifyNandle { get; set; }

        // Use rolen name as endpoint name. Bowlus.FrontEnd, or Bowlus.AppHost
        private string ServerEndpoint { set; get; }

        private string IpServerEndpoint { set; get; }

        private int retries = 0;

        public NotifyModuleServer(NotifyHandle callback)
        {
            this.TheNotifyNandle = callback;

            var roleName = RoleEnvironment.CurrentRoleInstance.Role.Name;
            this.ServerEndpoint = roleName;
            
            RoleInstanceEndpoint serviceEndPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[this.ServerEndpoint];
            this.IpServerEndpoint = serviceEndPoint.IPEndpoint.ToString();
        }

        // Note: this method shouldn't block. Server should return immediately, and do the actural work in another thread.
        // We deliberately choose to use our own thread instead of WCF thread, because we want to keep communication simple
        // by keeping only one WCF thread and only one service instance.
        public void Notify(NotifyMessageType msg)
        {
            Trace.TraceInformation(string.Format("*** {0}/{1} GOT Notification: {2}", this.IpServerEndpoint, this.ServerEndpoint, msg));
            Thread thread = new Thread(new ThreadStart(() => { TheNotifyNandle(msg); }));
            thread.Start();
        }

        public void StartNotifyService()
        {
            ServiceHost serviceHost = new ServiceHost(this);

            serviceHost.Faulted += (sender, e) =>
            {
                Trace.TraceInformation(string.Format("*** {0}/{1} HOST FAULT. Restarting the host. Retry {2}", IpServerEndpoint, ServerEndpoint, retries++));
                serviceHost.Abort();
                StartNotifyService();
            };

            NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);

            serviceHost.AddServiceEndpoint(
               typeof(INotifyService),
               binding,
               string.Format("net.tcp://{0}/{1}", this.IpServerEndpoint, this.ServerEndpoint));

            try
            {
                serviceHost.Open();
                Trace.TraceInformation(string.Format("*** {0}/{1} Notify Server Started.", this.IpServerEndpoint, this.ServerEndpoint));
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.Message);
            }
        }        
    }
}
